
// Copyright (C) 2009 Luca Piccioni
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Cache;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Xml;
using System.Xml.XPath;
using System.Xml.Xsl;

namespace Derm.BindGen
{
	/// <summary>
	/// Database collecting documentation information.
	/// </summary>
	class ManualDatabase
	{
		public static void LoadManuals(string path, SpecParser sParser)
		{
			List<Thread> manThreads = new List<Thread>();
			string[] manualFiles = Directory.GetFiles(path, "*.xml");

			foreach (string manFile in manualFiles) {
				try {
					ManualRoutine manRoutine = ManualRoutine.LoadManual(manFile, sParser);

					if ((manRoutine != null) && (sManRoutines.ContainsKey(manRoutine.Identifier) == false))
						sManRoutines[manRoutine.Identifier] = manRoutine;
				} catch (Exception eException) {
					Console.WriteLine("Unable to load manual from {0} ({1}).", manFile, eException.Message);
					Console.WriteLine("Exception stack trace: \n{0}", eException.StackTrace);
				}
			}
		}

		public static ManualRoutine GetManual(string importName)
		{
			if (sManRoutines.ContainsKey(importName) == true)
				return (sManRoutines[importName]);

			foreach (KeyValuePair<string, ManualRoutine> pair in sManRoutines) {
				ManualRoutine manRoutine = pair.Value;

				if (manRoutine.ReferenceNames.Contains(importName) == true)
					return (manRoutine);
			}

			return (null);
		}

		private static Dictionary<string, ManualRoutine> sManRoutines = new Dictionary<string,ManualRoutine>();
	}

	class ManualRoutine
	{
		public List<string> DocLines;

		public string Identifier = null;

		public List<string> ReferenceNames = new List<string>();

		public static ManualRoutine LoadManual(string path, SpecParser sParser)
		{
			ManualRoutine routine;

			Console.WriteLine("Loading manual page from {0}", Path.GetFileName(path));

			List<string> docLines = LoadDocLines(path, sParser, out routine);

			docLines = PatchDocLines(docLines);
			docLines = RenameDocLinesSymbols(docLines, sParser);
			//docLines = WrapDocLinesSymbols(docLines, 120);

			routine.DocLines = CommentDocLines(docLines);

			return (routine);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="path"></param>
		/// <param name="manRoutine"></param>
		/// <returns></returns>
		public static List<string> LoadDocLines(string path, SpecParser sParser, out ManualRoutine manRoutine)
		{
			Function manFunction = null;
			XmlDocument xml = new XmlDocument();
			XmlNodeList xmlNodeList;

			// Load XML file
			using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read)) {
				NameTable nt = new NameTable();
				XmlNamespaceManager nsmgr = new XmlNamespaceManager(nt);
				nsmgr.AddNamespace("mml", "http://www.w3.org/2001/XMLSchema-instance");
				XmlParserContext context = new XmlParserContext(null, nsmgr, null, XmlSpace.Default);

				XmlReaderSettings xmlReaderSettings = new XmlReaderSettings();
				xmlReaderSettings.ProhibitDtd = false;
				xmlReaderSettings.ConformanceLevel = ConformanceLevel.Auto;
				xmlReaderSettings.XmlResolver = new LocalXhtmlXmlResolver("../../Specs/Man/ManDTD");
				xmlReaderSettings.IgnoreComments = true;
				xmlReaderSettings.CheckCharacters = false;
				xmlReaderSettings.ValidationType = ValidationType.None;
				xmlReaderSettings.ValidationFlags = 0;

				XmlReader xmlReader = XmlReader.Create(fs, xmlReaderSettings, context);

				xml.Load(xmlReader);
				xmlReader.Close();
			}

			// Extra information
			XmlElement root = xml.DocumentElement;
			XmlNode xmlIdentifier;

			if (root.Name != "refentry")
				throw new InvalidOperationException("no refentry");

			manRoutine = new ManualRoutine();

			if (manRoutine.Identifier == null) {
				xmlIdentifier = root.SelectSingleNode("/refentry/refnamediv/refdescriptor");
				if (xmlIdentifier != null)
					manRoutine.Identifier = xmlIdentifier.InnerText;
			}

			if (manRoutine.Identifier == null) {
				xmlIdentifier = root.SelectSingleNode("/refentry/refnamediv/refname");
				if (xmlIdentifier != null)
					manRoutine.Identifier = xmlIdentifier.InnerText;
			}

			xmlNodeList = root.SelectNodes("/refentry/refnamediv/refname");
			foreach (XmlNode node in xmlNodeList)
				manRoutine.ReferenceNames.Add(node.InnerText);

			// Insert function specific information
			if (sParser.Functs.ContainsKey(manRoutine.Identifier) == true)
				manFunction = sParser.Functs[manRoutine.Identifier];

			if (manFunction != null) {
#if false
				xmlParentNode = root.SelectSingleNode("/refentry/refsect1[@id='description']");
				if (xmlParentNode != null) {
					xmlRefNode = root.SelectSingleNode("/refentry/refsect1[@id='description']/para[position()=last()]");
					if (xmlRefNode != null) {
						if (manFunction.IsDeprecated == true) {
							xmlNode = xml.CreateElement("para");
							xmlNode.InnerXml = "This function is DEPRECATED for a forward compatible context.";
							xmlParentNode.InsertBefore(xmlNode, xmlRefNode);
						}
						if (manFunction.Category != null) {
							xmlNode = xml.CreateElement("para");
							xmlNode.InnerXml = "This function belongs to '" + manFunction.Category + "'.";
							xmlParentNode.InsertBefore(xmlNode, xmlRefNode);
						}
						if (manFunction.SubsetFunctions.Count > 0) {
							StringBuilder sb = new StringBuilder();

							xmlNode = xml.CreateElement("para");
							sb.AppendLine("Depending on driver implementation, this routine could call the following (equivalent) routines:");
							sb.AppendLine("- " + manFunction.ImportName);
							foreach (Function subset in manFunction.SubsetFunctions)
								sb.AppendLine("- " + subset.ImportName);
							xmlNode.InnerXml = sb.ToString();
							xmlParentNode.InsertBefore(xmlNode, xmlRefNode);
						}
					}
				}
#endif
			}

			// Transform XML into C# documentation
			XslCompiledTransform xslTranform = new XslCompiledTransform(true);
			string inlineDocs;

			using (StringWriter sw = new StringWriter()) {

				xslTranform.Load("../../Specs/Man/ManXsl/CsCodeDoc.xslt");
				xslTranform.Transform(root.CreateNavigator(), null, sw);

				inlineDocs = sw.ToString();
			}

			return (new List<string>(inlineDocs.Split(new char[]{'\n', '\r'}, StringSplitOptions.RemoveEmptyEntries)));
		}

		public static List<string> RenameDocLinesSymbols(List<string> docLines, SpecParser sParser)
		{
			List<string> pLines = new List<string>();
			Regex rLinkRef = new Regex("<(see|seealso) cref=\"[A-Za-z_0-9\\{\\}\\(\\)\\|\\* ]*\" />");
			Regex rLinkRefCheck = new Regex("<(see|seealso)");

			foreach (string line in docLines) {
				MatchCollection rLinkRefMatchCollection;
				string docLine = line;

				// Adjust MathML markup (remove namespaces)
				docLine = docLine.Replace("<mml:", "<");
				docLine = docLine.Replace("</mml:", "</");

				docLine = docLine.Replace("<informalequation><math ", "<math display=\"block\" ");
				docLine = docLine.Replace("</informalequation>", "");
				docLine = docLine.Replace("<inlineequation><math ", "<math display=\"inline\" ");
				docLine = docLine.Replace("</inlineequation>", "");

				// Adjust <see and <seealso tags
				if ((rLinkRefMatchCollection = rLinkRef.Matches(docLine)).Count > 0) {
					int offset = 0;

					foreach (Match rLinkRefMatch in rLinkRefMatchCollection) {
						int tagNameBegin = rLinkRefMatch.Index + 1 + offset;
						int tagNameEnd = docLine.IndexOf(' ', tagNameBegin);
						int nameBegin = docLine.IndexOf("\"", tagNameBegin) + 1;
						int nameEnd = docLine.IndexOf("\"", nameBegin);
						int tagEnd = docLine.IndexOf('>', tagNameBegin);

						string tag = docLine.Substring(tagNameBegin, tagNameEnd - tagNameBegin);
						string name = docLine.Substring(nameBegin, nameEnd - nameBegin);

						string origString = docLine.Substring(tagNameBegin - 1, tagEnd - tagNameBegin + 2);
						string aliasString = null;

						if ((name.StartsWith(sParser.FunctPrefix) == true) && ((sParser.FunctPrefix == "gl") && (name.StartsWith("glX") == false))) {
							Function function = sParser.MapFunction(name);

							if (function != null)
								aliasString = String.Format("<{0} cref=\"{1}.{2}\"/>", tag, sParser.Namespace, function.ImplementationName);
							else {
								if (name.Length > 3) {
									aliasString = "<i>"+sParser.Namespace+"."+name.Substring(sParser.FunctPrefix.Length)+"</i>";
									Console.WriteLine("		Unknown function: {0}", name);
								} else {
									aliasString = "<i>"+name+"</i>";
									Console.WriteLine("		Unknown simil-function symbol: {0}", name);
								}
							}

						} else if (name.StartsWith(sParser.EnumPrefix) == true) {
							Enum.Value value = sParser.MapEnumValue(name);

							if (value != null) {
								aliasString = String.Format("<{0} cref=\"{1}.{2}\"/>", tag, sParser.Namespace, value.ImplementationName);
							} else {
								aliasString = "<i>"+sParser.Namespace+"."+name.Substring(sParser.EnumPrefix.Length)+"</i>";
								Console.WriteLine("		Unknown enum value: {0}", name);
							}

						} else {
							bool specialCase = false;

							if (name == "NULL") {
								name = "null";
								specialCase = true;
							}

							aliasString = "<i>"+name+"</i>";
							if (specialCase == false)
								Console.WriteLine("		Unknown token: {0}", origString);
						}

						if (aliasString != null) {
							docLine = docLine.Replace(origString, aliasString);
							offset += aliasString.Length - origString.Length;
						}
					}
				} else {
					if (rLinkRefCheck.Match(docLine).Success == true) {
						Console.WriteLine("		Tag 'see' or 'seealso' not recognized in line '{0}'", docLine);
					}
				}
					
				pLines.Add(docLine);
			}

			return (pLines);
		}

		public static List<string> PatchDocLines(List<string> docLines)
		{
			List<string> pLines = new List<string>();

			for (int i = 0; i < docLines.Count; i++) {
				if (docLines[i].Trim() == "-") {

					// Note: this condition is very frequent!

					// Add line concatenated with the successive one
					pLines.Add(docLines[i] + docLines[i+1].TrimStart());
					// Skip next line
					i++;

				} else if (docLines[i].Trim() == "@endhtmlonly") {

					// Note: this condition is very frequent!

					pLines[pLines.Count - 1] = pLines[pLines.Count - 1] + "@endhtmlonly";

				} else {
					// Add line
					pLines.Add(docLines[i]);
				}
			}

			return (pLines);
		}

		public static List<string> PatchDocExamples(List<string> docLines, SpecParser sParser)
		{
			Regex rDocToken = new Regex("[gl|glX|wgl|GL_|XGL_|WGL_].*");
			List<string> pLines = new List<string>();
			bool tagCode = false;

			foreach (string line in docLines) {
				if (line == @"\code")
					tagCode = true;
				if (line == @"\endcode")
					tagCode = false;

				if (tagCode == true) {
					string[] codeTokens = line.Split('\t', ' ');
					string pCodeLine = String.Empty;

					foreach (string token in codeTokens) {
						string codeToken = token;

						if (rDocToken.Match(token).Success == true) {
							string className = null;
							int prefixLength = 0;

							if ((token.StartsWith("gl") == true) || (token.StartsWith("GL_"))) {
								className = "Gl";
							} else if ((token.StartsWith("wgl") == true) || (token.StartsWith("WGL_") == true)) {
								className = "Wgl";
							} else if ((token.StartsWith("glX") == true) || (token.StartsWith("GLX_") == true)) {
								className = "Glx";
							} else
								throw new InvalidOperationException();

							if ((token.StartsWith("gl") == true) || (token.StartsWith("GL_"))) {
								prefixLength = 2;
							} else if ((token.StartsWith("wgl") == true) || (token.StartsWith("glX") == true) || (token.StartsWith("GL_") == true)) {
								prefixLength = 3;
							} else
								prefixLength = 4;

							codeToken = className + "." + token.Substring(prefixLength);
						}

						pCodeLine = pCodeLine + token;
					}

					pLines.Add(pCodeLine);
				} else
					pLines.Add(line);
			}

			return (pLines);
		}

		public static List<string> WrapDocLinesSymbols(List<string> docLines, int width)
		{
			List<string> pLines = new List<string>();
			bool tagCode = false;

			foreach (string line in docLines) {
				if (line.Contains("@code"))
					tagCode = true;
				if (line.Contains("@endcode"))
					tagCode = false;

				if (tagCode == false) {
					if (line.Length > width) {
						string pLine = line;
						string pLineWrapped = String.Empty;				// First token shall be "///"

						// Insert multiple lines
						string[] tokens = pLine.Split(' ');

						for (int i = 0; i < tokens.Length; i++) {
							if ((pLineWrapped.Length >= width) && (IsSpecialWrapToken(tokens[i]) == false)) {
								pLines.Add(pLineWrapped.Trim());
								pLineWrapped = String.Empty;
							}
							pLineWrapped += " " + tokens[i];
						}
						if (pLineWrapped.Length > 0)
							pLines.Add(pLineWrapped.Trim());
					} else
						pLines.Add(line);
				} else {
					pLines.Add(line);
				}
			}

			return (pLines);
		}

		private static bool IsSpecialWrapToken(string token)
		{
			if (token.EndsWith("/>") == true)
				return (true);

			return (false);
		}

		public static List<string> CommentDocLines(List<string> docLines)
		{
			List<string> pLines = new List<string>();
			bool tagCode = false;
			
			foreach (string docLine in docLines) {
				if (docLine.Contains("@code"))
					tagCode = true;
				if (docLine.Contains("@endcode"))
					tagCode = false;

				if (tagCode == false) {
					pLines.Add("/// " + docLine.TrimStart('\t', ' '));
				} else {
					pLines.Add("/// " + docLine.TrimStart('\t'));
				}
			}

			return (pLines);
		}
	}

	class LocalXhtmlXmlResolver : XmlUrlResolver
	{
		static LocalXhtmlXmlResolver()
		{
			KnownUris["-//OASIS//DTD DocBook MathML Module V1.1b1//EN"] = "http://www.oasis-open.org/docbook/xml/mathml/1.1CR1/dbmathml.dtd";
			LocalDtdPaths[KnownUris["-//OASIS//DTD DocBook MathML Module V1.1b1//EN"]] = "dbmathml.dtd";

			KnownUris["-//OASIS//DTD DocBook XML V4.3//EN"] = "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd";
			LocalDtdPaths[KnownUris["-//OASIS//DTD DocBook XML V4.3//EN"]] = "docbookx.dtd";

			KnownUris["-//OASIS//ENTITIES DocBook Notations V4.3//EN"] = "http://www.oasis-open.org/docbook/xml/4.3/dbnotnx.mod";
			LocalDtdPaths[KnownUris["-//OASIS//ENTITIES DocBook Notations V4.3//EN"]] = "dbnotnx.mod";

			KnownUris["-//OASIS//ENTITIES DocBook Character Entities V4.3//EN"] = "http://www.oasis-open.org/docbook/xml/4.3/dbcentx.mod";
			LocalDtdPaths[KnownUris["-//OASIS//ENTITIES DocBook Character Entities V4.3//EN"]] = "dbcentx.mod";

			KnownUris["-//W3C//DTD XHTML 1.0 Transitional//EN"] = "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd";
			LocalDtdPaths[KnownUris["-//W3C//DTD XHTML 1.0 Transitional//EN"]] = "xhtml1-transitional.dtd";
		}

		private static readonly Dictionary<string, string> KnownUris = new Dictionary<string, string>();
		private static readonly Dictionary<string, string> LocalDtdPaths = new Dictionary<string, string>();

		private static readonly Dictionary<string, string> LocalDtdRelPaths = new Dictionary<string, string>();

		
		private string mDtdPath;

		public LocalXhtmlXmlResolver(string dtdPath)
		{
			string[] dtdFiles;

			mDtdPath = dtdPath;

			dtdFiles = Directory.GetFiles(dtdPath, "*.dtd");
			foreach (string dtdFile in dtdFiles)
				LocalDtdRelPaths[dtdFile] = dtdFile.Replace('\\', '/');

			dtdFiles = Directory.GetFiles(dtdPath, "*.mod");
			foreach (string dtdFile in dtdFiles)
				LocalDtdRelPaths[dtdFile] = dtdFile.Replace('\\', '/');

			dtdFiles = Directory.GetFiles(dtdPath, "*.ent");
			foreach (string dtdFile in dtdFiles)
				LocalDtdRelPaths[dtdFile] = dtdFile.Replace('\\', '/');
		}

		public Uri ResolveUri(Uri baseUri, string relativeUri)
		{
			return KnownUris.ContainsKey(relativeUri) ?
				new Uri(KnownUris[relativeUri]) :
				base.ResolveUri(baseUri, relativeUri)
				;
		}

		public object GetEntity(Uri absoluteUri, string role, Type ofObjectToReturn)
		{
			if (absoluteUri == null)
				throw new ArgumentNullException("absoluteUri");

			//resolve resources from cache (if possible)
			if ((absoluteUri.Scheme == "http") && (ofObjectToReturn == null || ofObjectToReturn == typeof(Stream))) {
				string localPath = absoluteUri.OriginalString.Substring(absoluteUri.OriginalString.LastIndexOf("/") + 1);
				bool relative = false;

				foreach (string key in LocalDtdRelPaths.Keys) {
					if (key.EndsWith(localPath) == true) {
						LocalDtdPaths[absoluteUri.OriginalString] = localPath;
						relative = true;
						break;
					}
				}


				if ((relative == false) && (LocalDtdPaths.ContainsKey(absoluteUri.OriginalString) == false)) {
					Console.Write("Downloading {0}...", absoluteUri);

					WebRequest webRequest;
					WebResponse webResponse = null;
					int tries = 0;
					bool done = false;

					do {
						try {
							webRequest = WebRequest.Create(absoluteUri);
							webRequest.CachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.Default);
							webRequest.Timeout = 1000;
							webResponse = webRequest.GetResponse();
							done = true;
							Console.WriteLine(". done!");
						} catch (Exception e) {
							Console.Write(".");
							tries++;
						}
					} while ((done == false) && (tries < 3));

					if (done == true) {
						localPath = localPath.Substring(localPath.LastIndexOf("/") + 1);

						using (StreamWriter fs = new StreamWriter(Path.Combine(mDtdPath, localPath), false)) {
							Stream rStream = webResponse.GetResponseStream();

							using (StreamReader sr = new StreamReader(rStream)) {
								string contents = sr.ReadToEnd();

								fs.Write(contents);
							}
						}

						LocalDtdPaths[absoluteUri.OriginalString] = localPath;
					} else {
						Console.WriteLine("not done");
						return null;
					}
				}

				return (new FileStream(Path.Combine(mDtdPath, LocalDtdPaths[absoluteUri.OriginalString]), FileMode.Open, FileAccess.Read));
			}

			//otherwise use the default behavior of the XmlUrlResolver class (resolve resources from source)
			return base.GetEntity(absoluteUri, role, ofObjectToReturn);
		}
	}
}
